Custom Component Renderers for Flow UI Extensibility#471
Custom Component Renderers for Flow UI Extensibility#471brionmario merged 3 commits intoasgardeo:mainfrom
Conversation
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 52 minutes and 39 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThis PR adds an extension mechanism for custom component renderers in the Asgardeo SDK. It introduces new configuration interfaces, types for component rendering context, and React context infrastructure to allow consumers to register custom render functions for flow components, both known and unknown types. Changes
Sequence DiagramsequenceDiagram
participant App as Application
participant ASP as AsgardeoProvider
participant CRP as ComponentRendererProvider
participant CTX as ComponentRendererContext
participant CAF as AuthOptionFactory
participant CR as Custom Renderer
App->>ASP: Pass extensions config with renderers
ASP->>CRP: Wrap with ComponentRendererProvider(renderers)
CRP->>CTX: Set context value to renderers map
App->>CAF: Render flow component
CAF->>CTX: useContext to read renderers map
alt Custom renderer exists
CAF->>CAF: Build ComponentRenderContext
CAF->>CR: Invoke customRenderer(component, context)
CR-->>CAF: Return custom rendered element
else No custom renderer
CAF->>CAF: Use default switch-based rendering
end
CAF-->>App: Rendered component
Estimated Code Review Effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly Related Issues
Possibly Related PRs
Suggested Reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🦋 Changeset detectedThe changes in this PR will be included in the next version bump. Not sure what this means? Click here to learn what changesets are. |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
packages/react/src/contexts/ComponentRenderer/ComponentRendererContext.ts (1)
19-41: Eliminate the duplicated renderer contract by aliasing the shared types.The React package duplicates
ComponentRenderContextandComponentRendererwhich are already defined in the JavaScript SDK (@asgardeo/javascript) and re-exported via@asgardeo/browser. Aliasing these shared types prevents future drift between configured renderers and the runtime context.♻️ Proposed type aliasing
-import {EmbeddedFlowComponentV2 as EmbeddedFlowComponent, FlowMetadataResponse} from '@asgardeo/browser'; +import type { + ComponentRenderContext, + ComponentRenderer, +} from '@asgardeo/browser'; import {createContext, ReactElement} from 'react'; -export interface ComponentRenderContext { - additionalData?: Record<string, any>; - authType: 'signin' | 'signup'; - formErrors: Record<string, string>; - formValues: Record<string, string>; - isFormValid: boolean; - isLoading: boolean; - meta?: FlowMetadataResponse | null; - onInputBlur?: (name: string) => void; - onInputChange: (name: string, value: string) => void; - onSubmit?: (component: EmbeddedFlowComponent, data?: Record<string, any>, skipValidation?: boolean) => void; - touchedFields: Record<string, boolean>; -} +export type {ComponentRenderContext} from '@asgardeo/browser'; -export type ComponentRenderer = ( - component: EmbeddedFlowComponent, - context: ComponentRenderContext, -) => ReactElement | null; +export type {ComponentRenderer} from '@asgardeo/browser';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/react/src/contexts/ComponentRenderer/ComponentRendererContext.ts` around lines 19 - 41, Replace the duplicated local types with aliases to the shared SDK types: import ComponentRenderContext and ComponentRenderer from '@asgardeo/browser' (alongside EmbeddedFlowComponent and FlowMetadataResponse), then remove the local interface and type declarations and re-export them as type ComponentRenderContext = ImportedComponentRenderContext and type ComponentRenderer = ImportedComponentRenderer; keep ComponentRendererMap (which can continue to reference ComponentRenderer) and ensure function/type names referenced are ComponentRenderContext, ComponentRenderer, ComponentRendererMap, EmbeddedFlowComponent, and FlowMetadataResponse so the module uses the SDK types instead of duplicating the contract.packages/javascript/src/models/v2/extensions/components.ts (1)
90-95: Propagate the renderer element type throughComponentsExtensions.
ComponentRendereris generic, butComponentsExtensions.renderersfixes it tounknown, requiring React to erase types withas anyin AsgardeoProvider.tsx. MakingComponentsExtensionsgeneric with a default type parameter maintains backward compatibility while enabling proper type specialization:♻️ Proposed type refinement
-export interface ComponentsExtensions { +export interface ComponentsExtensions<TElement = unknown> { /** * Custom renderers keyed by flow component type. */ - renderers?: Record<string, ComponentRenderer<unknown>>; + renderers?: Record<string, ComponentRenderer<TElement>>; }The default type parameter preserves backward compatibility for all existing usages. This eliminates the type erasure cast in React and allows framework packages to specialize the element type when needed.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/javascript/src/models/v2/extensions/components.ts` around lines 90 - 95, Make ComponentsExtensions generic so the renderer element type is propagated: change the declaration of ComponentsExtensions to accept a type parameter with a default (e.g., export interface ComponentsExtensions<E = unknown>) and update renderers to use that type (renderers?: Record<string, ComponentRenderer<E>>). This preserves backward compatibility while allowing callers (and files like AsgardeoProvider.tsx) to specialize the element type instead of casting to any.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/react/src/components/presentation/auth/AuthOptionFactory.tsx`:
- Around line 217-218: The factory createAuthComponentFromFlow must not call
hooks directly; remove useContext(ComponentRendererContext) and useTheme() usage
inside that function and instead make createAuthComponentFromFlow a pure
function that receives theme and customRenderers as parameters (or create a
separate custom hook wrapper, e.g., useAuthComponentFactory, that calls useTheme
and useContext once and passes those values into createAuthComponentFromFlow);
update all callers to obtain theme/customRenderers from the hook/component and
forward them into createAuthComponentFromFlow so hook order no longer depends on
the JSON structure.
- Around line 222-238: The lookup of customRenderer should ensure entries come
from customRenderers' own properties and are functions: instead of directly
reading customRenderers[component.id] or [component.type], check
Object.prototype.hasOwnProperty.call(customRenderers, component.id) or
component.type and that the resolved value is typeof 'function' before assigning
to customRenderer; then only call customRenderer(component, renderCtx) when both
checks pass (references: customRenderers, customRenderer, component.id,
component.type, renderCtx, and the return call).
---
Nitpick comments:
In `@packages/javascript/src/models/v2/extensions/components.ts`:
- Around line 90-95: Make ComponentsExtensions generic so the renderer element
type is propagated: change the declaration of ComponentsExtensions to accept a
type parameter with a default (e.g., export interface ComponentsExtensions<E =
unknown>) and update renderers to use that type (renderers?: Record<string,
ComponentRenderer<E>>). This preserves backward compatibility while allowing
callers (and files like AsgardeoProvider.tsx) to specialize the element type
instead of casting to any.
In `@packages/react/src/contexts/ComponentRenderer/ComponentRendererContext.ts`:
- Around line 19-41: Replace the duplicated local types with aliases to the
shared SDK types: import ComponentRenderContext and ComponentRenderer from
'@asgardeo/browser' (alongside EmbeddedFlowComponent and FlowMetadataResponse),
then remove the local interface and type declarations and re-export them as type
ComponentRenderContext = ImportedComponentRenderContext and type
ComponentRenderer = ImportedComponentRenderer; keep ComponentRendererMap (which
can continue to reference ComponentRenderer) and ensure function/type names
referenced are ComponentRenderContext, ComponentRenderer, ComponentRendererMap,
EmbeddedFlowComponent, and FlowMetadataResponse so the module uses the SDK types
instead of duplicating the contract.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8c20867d-6667-4432-9b22-f4815dc70612
📒 Files selected for processing (8)
.changeset/purple-paths-retire.mdpackages/javascript/src/index.tspackages/javascript/src/models/config.tspackages/javascript/src/models/v2/extensions/components.tspackages/react/src/components/presentation/auth/AuthOptionFactory.tsxpackages/react/src/contexts/Asgardeo/AsgardeoProvider.tsxpackages/react/src/contexts/ComponentRenderer/ComponentRendererContext.tspackages/react/src/contexts/ComponentRenderer/ComponentRendererProvider.tsx
| const {theme} = useTheme(); | ||
| const customRenderers = useContext(ComponentRendererContext); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Inspect hook calls inside AuthOptionFactory and where the render helpers are invoked.
rg -n -C3 '\b(useContext|useTheme)\s*\(' packages/react/src/components/presentation/auth/AuthOptionFactory.tsx
rg -n -C3 '\b(renderSignInComponents|renderSignUpComponents|renderInviteUserComponents)\s*\(' --type=ts --type=tsxRepository: asgardeo/javascript
Length of output: 374
🏁 Script executed:
# Read the function definition and surrounding context
cat -n packages/react/src/components/presentation/auth/AuthOptionFactory.tsx | sed -n '200,240p'Repository: asgardeo/javascript
Length of output: 1842
🏁 Script executed:
# Search for the function definition
rg -n 'createAuthComponentFromFlow' packages/react/src/components/presentation/auth/AuthOptionFactory.tsx -A 5 -B 5Repository: asgardeo/javascript
Length of output: 2058
🏁 Script executed:
# Check if there are recursive calls within the function
rg -n 'createAuthComponentFromFlow\s*\(' packages/react/src/components/presentation/auth/AuthOptionFactory.tsxRepository: asgardeo/javascript
Length of output: 269
🏁 Script executed:
# Find where createAuthComponentFromFlow is called at the top level (not recursively)
rg -n 'renderSignInComponents|renderSignUpComponents|renderInviteUserComponents' packages/react/src/components/presentation/auth/AuthOptionFactory.tsx -B 2 -A 15 | head -50Repository: asgardeo/javascript
Length of output: 2136
🏁 Script executed:
# Check which React component wraps these render functions
rg -n 'function\s+\w+|const\s+\w+\s*=\s*\(' packages/react/src/components/presentation/auth/AuthOptionFactory.tsx | head -20Repository: asgardeo/javascript
Length of output: 902
🏁 Script executed:
# Check if these exported render functions are called from React components
rg -n 'renderSignInComponents|renderSignUpComponents|renderInviteUserComponents' packages/react/src/components/presentation/auth/ --type=ts --type=tsx -lRepository: asgardeo/javascript
Length of output: 90
🏁 Script executed:
# Look at a caller to see how it's invoked
rg -n 'renderSignInComponents|renderSignUpComponents' packages/react/src/components/presentation/auth/ -B 3 -A 3 | head -80Repository: asgardeo/javascript
Length of output: 7397
🏁 Script executed:
# Check how renderComponents is used inside the useCallback
rg -n 'renderComponents' packages/react/src/components/presentation/auth/SignIn/v2/BaseSignIn.tsx -B 2 -A 10 | head -60Repository: asgardeo/javascript
Length of output: 924
Do not call context hooks inside this recursive factory.
createAuthComponentFromFlow runs once per flow component and recursively for blocks/stacks, so adding useContext and useTheme here makes React hook order depend on server-provided JSON shape. Hooks must only be called from React components or custom hooks, not from plain factory functions. Read ComponentRendererContext and theme from a component/custom hook once and pass them into this pure factory.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/react/src/components/presentation/auth/AuthOptionFactory.tsx` around
lines 217 - 218, The factory createAuthComponentFromFlow must not call hooks
directly; remove useContext(ComponentRendererContext) and useTheme() usage
inside that function and instead make createAuthComponentFromFlow a pure
function that receives theme and customRenderers as parameters (or create a
separate custom hook wrapper, e.g., useAuthComponentFactory, that calls useTheme
and useContext once and passes those values into createAuthComponentFromFlow);
update all callers to obtain theme/customRenderers from the hook/component and
forward them into createAuthComponentFromFlow so hook order no longer depends on
the JSON structure.
| const customRenderer = customRenderers[component.id] ?? customRenderers[component.type as string]; | ||
| if (customRenderer) { | ||
| const renderCtx: ComponentRenderContext = { | ||
| additionalData: options.additionalData, | ||
| authType, | ||
| formErrors, | ||
| formValues, | ||
| isFormValid, | ||
| isLoading, | ||
| meta: options.meta, | ||
| onInputBlur: options.onInputBlur, | ||
| onInputChange, | ||
| onSubmit: options.onSubmit, | ||
| touchedFields, | ||
| }; | ||
| return customRenderer(component, renderCtx); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Demonstrate that plain object bracket lookup can resolve inherited properties.
node - <<'NODE'
const customRenderers = {};
const component = { id: 'toString', type: 'TEXT' };
const customRenderer = customRenderers[component.id] ?? customRenderers[component.type];
console.log({
isInheritedToString: customRenderer === Object.prototype.toString,
type: typeof customRenderer,
});
NODERepository: asgardeo/javascript
Length of output: 110
🏁 Script executed:
#!/bin/bash
# Locate the file and check if it exists
fd "AuthOptionFactory.tsx" --type fRepository: asgardeo/javascript
Length of output: 132
🏁 Script executed:
#!/bin/bash
# Read the specific lines to verify the code matches the review
sed -n '222,238p' packages/react/src/components/presentation/auth/AuthOptionFactory.tsxRepository: asgardeo/javascript
Length of output: 607
🏁 Script executed:
#!/bin/bash
# Read more lines to see how customRenderer is used
sed -n '222,250p' packages/react/src/components/presentation/auth/AuthOptionFactory.tsxRepository: asgardeo/javascript
Length of output: 1027
🏁 Script executed:
#!/bin/bash
# Find where customRenderers is defined/used in this function
head -n 240 packages/react/src/components/presentation/auth/AuthOptionFactory.tsx | tail -n 50Repository: asgardeo/javascript
Length of output: 2011
🏁 Script executed:
#!/bin/bash
# Find ComponentRenderer and ComponentRendererMap type definitions
fd "ComponentRenderer" --type f -e ts -e tsx | head -20Repository: asgardeo/javascript
Length of output: 212
🏁 Script executed:
#!/bin/bash
# Read the ComponentRendererContext to see type definitions
cat packages/react/src/contexts/ComponentRenderer/ComponentRendererContext.tsRepository: asgardeo/javascript
Length of output: 1763
Guard renderer lookup to own function-valued registrations.
With the default {} renderer map, an id/type like toString or constructor resolves from Object.prototype, so a server-provided component can bypass built-in rendering and invoke a non-renderer. Check own properties and typeof renderer === 'function' before calling.
🛡️ Proposed safer lookup
import ComponentRendererContext, {
ComponentRenderContext,
+ ComponentRenderer,
} from '../../../contexts/ComponentRenderer/ComponentRendererContext';- const customRenderer: ComponentRenderer | undefined =
- customRenderers[component.id] ?? customRenderers[component.type as string];
+ const getCustomRenderer = (rendererKey?: string): ComponentRenderer | undefined => {
+ if (!rendererKey || !Object.prototype.hasOwnProperty.call(customRenderers, rendererKey)) {
+ return undefined;
+ }
+
+ const renderer: ComponentRenderer = customRenderers[rendererKey];
+
+ return typeof renderer === 'function' ? renderer : undefined;
+ };
+
+ const customRenderer = getCustomRenderer(component.id) ?? getCustomRenderer(component.type as string);
if (customRenderer) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const customRenderer = customRenderers[component.id] ?? customRenderers[component.type as string]; | |
| if (customRenderer) { | |
| const renderCtx: ComponentRenderContext = { | |
| additionalData: options.additionalData, | |
| authType, | |
| formErrors, | |
| formValues, | |
| isFormValid, | |
| isLoading, | |
| meta: options.meta, | |
| onInputBlur: options.onInputBlur, | |
| onInputChange, | |
| onSubmit: options.onSubmit, | |
| touchedFields, | |
| }; | |
| return customRenderer(component, renderCtx); | |
| } | |
| const getCustomRenderer = (rendererKey?: string): ComponentRenderer | undefined => { | |
| if (!rendererKey || !Object.prototype.hasOwnProperty.call(customRenderers, rendererKey)) { | |
| return undefined; | |
| } | |
| const renderer: ComponentRenderer = customRenderers[rendererKey]; | |
| return typeof renderer === 'function' ? renderer : undefined; | |
| }; | |
| const customRenderer = getCustomRenderer(component.id) ?? getCustomRenderer(component.type as string); | |
| if (customRenderer) { | |
| const renderCtx: ComponentRenderContext = { | |
| additionalData: options.additionalData, | |
| authType, | |
| formErrors, | |
| formValues, | |
| isFormValid, | |
| isLoading, | |
| meta: options.meta, | |
| onInputBlur: options.onInputBlur, | |
| onInputChange, | |
| onSubmit: options.onSubmit, | |
| touchedFields, | |
| }; | |
| return customRenderer(component, renderCtx); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/react/src/components/presentation/auth/AuthOptionFactory.tsx` around
lines 222 - 238, The lookup of customRenderer should ensure entries come from
customRenderers' own properties and are functions: instead of directly reading
customRenderers[component.id] or [component.type], check
Object.prototype.hasOwnProperty.call(customRenderers, component.id) or
component.type and that the resolved value is typeof 'function' before assigning
to customRenderer; then only call customRenderer(component, renderCtx) when both
checks pass (references: customRenderers, customRenderer, component.id,
component.type, renderCtx, and the return call).
Purpose
Introduce support for customizing SDK UI components using the
ComponentRendererextension point in@asgardeo/react.This enables applications to override default components and build fully customized experiences on top of the authentication flow.
Usage Example
Below is an example of how to override a default component using a custom renderer.
Expected Output
Flow Graph
Click to see the flow graph
{ "executionId": "019db150-0eb5-799e-9d9f-5bbe6fce26fa", "flowStatus": "INCOMPLETE", "type": "VIEW", "challengeToken": "2cbad16bd9264ac96f719b27c7bd2ba5daa0333bcf285181ccd1f5e4f6ca159d", "data": { "actions": [ { "ref": "action_v9hh", "nextNode": "ID_g5do" } ], "meta": { "components": [ { "category": "DISPLAY", "id": "text_69uy", "label": "Sign In", "resourceType": "ELEMENT", "type": "TEXT", "variant": "HEADING_3" }, { "category": "BLOCK", "components": [ { "category": "MISCELLANEOUS", "id": "LOGIN_ID", "resourceType": "ELEMENT", "type": "CUSTOM" }, { "category": "ACTION", "eventType": "SUBMIT", "id": "action_v9hh", "label": "Continue", "resourceType": "ELEMENT", "type": "ACTION", "variant": "PRIMARY" }, { "category": "DISPLAY", "id": "rich_text_la4l", "label": "\u003cp class=\"rich-text-paragraph\"\u003e\u003cspan class=\"rich-text-pre-wrap\"\u003eDon't have an account? \u003c/span\u003e\u003ca href=\"{{meta(application.sign_up_url)}}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"rich-text-link\"\u003e\u003cspan class=\"rich-text-pre-wrap\"\u003eSign up\u003c/span\u003e\u003c/a\u003e\u003c/p\u003e", "resourceType": "ELEMENT", "type": "RICH_TEXT" } ], "id": "block_l7ea", "resourceType": "ELEMENT", "type": "BLOCK" } ] } } }1. Register a custom renderer via
AsgardeoProvider2. Example of the
customRendererFlow Graph
Notes
componentcontains metadata about the rendered component.contextprovides:formValues→ current form stateonInputChange(field, value)→ update form valuesisLoading→ loading stateRelated Issues
Related PRs
Checklist
Security checks
Summary by CodeRabbit
New Features
Documentation